package eval.engine;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import eval.deploy.IDemandManager;
import eval.deploy.IDeployManager;
import eval.host.Host;
import eval.host.IHostManager;
import eval.host.compare.ISortHostList;
import eval.hostEvaluate.IHostEvaluate;
import eval.utilities.configuration.ExperimentConfiguration;
import eval.vm.IVMManager;
import eval.vm.VMIdentifier;
import eval.vm.VirtualMachine;

public class BasicEngineModel {
	
	protected ExperimentConfiguration configuration = ExperimentConfiguration.getSingleton();
	protected Map<Host, List<VirtualMachine>> vmMapping = new HashMap<Host, List<VirtualMachine>>(configuration.totalNumberOfHosts);
	protected ISortHostList sortHostList;
	protected IDeployManager deployManager;
	protected IHostManager hostManager;
	protected IVMManager vmManager;
	protected IDemandManager demandManager;
	protected List<Host> hosts;
	protected IHostEvaluate hostEvaluator;
	protected long engineStartTime = 0;
	protected long engineStopTime = 0;
	protected int[] HistoryDemandArray;
	protected int tick;
	
	
	public BasicEngineModel(IVMManager vmManager, IDeployManager deployManager,
			IHostManager hostManager, IDemandManager demandManager,
			ISortHostList sortHostList,IHostEvaluate hostEvaluator) {
		this.deployManager = deployManager;
		this.hostManager = hostManager;
		this.vmManager = vmManager;
		this.demandManager = demandManager;
		this.sortHostList = sortHostList;
		this.hostEvaluator = hostEvaluator;
		this.hosts = hostManager.getHosts();
		this.HistoryDemandArray = new int[configuration.experimentTicks];
		this.vmMapping = new HashMap<Host, List<VirtualMachine>>(configuration.totalNumberOfHosts);
		for (Host host : hosts)
			vmMapping.put(host, new ArrayList<VirtualMachine>());
	}
	
	protected void removeVirtualMachines(List<VirtualMachine> vmsToRemove) {
		if(!vmsToRemove.isEmpty())
			hostManager.removeListOfVmFromHosts(vmsToRemove);
	}
	
	protected void iniCloudMapping(IPlacement lastPlacement,
			List<VirtualMachine> vmsToRemove,
			Map<VirtualMachine, Integer> cpuDemandOfVMs,
			Map<VirtualMachine, Integer> memDemandOfVMs) {
		Map<Host, List<VirtualMachine>> oldMap = lastPlacement.getvmMapping();
		if(oldMap!=null) {
			int cpuLoadOnHost,memLoadOnHost,vmCpuLoad,vmMemLoad;
			for(Map.Entry<Host, List<VirtualMachine>> oldEntry : oldMap.entrySet()) {
				cpuLoadOnHost = memLoadOnHost=0;
				//give to each virtual machine  minimum(minimum CPU, demand CPU )
				for(VirtualMachine vm : oldEntry.getValue()) {
					if(!vmsToRemove.contains(vm)) {
						vmCpuLoad = Math.min(cpuDemandOfVMs.get(vm).intValue(),vm.getVMConfiguration().getMinCpu());
						vmMemLoad = memDemandOfVMs.get(vm).intValue();
						vmManager.setSatisfiedLoad(vm, vmCpuLoad, vmMemLoad);
						cpuLoadOnHost += vmCpuLoad;
						memLoadOnHost += vmMemLoad;
					}
				}
				hostManager.setHostCurrentLoad(oldEntry.getKey(),cpuLoadOnHost, memLoadOnHost);
			}
		}
	}
	
	protected void updateVmMap() {
		for(Host host : hosts) {
			if(host.isOpen()) {
				vmMapping.put(host, hostManager.getVmsAtHost(host));
			}
		}
	}
	
	protected int calcCloudGraphGradient() {
		
		int historyWindowLength = configuration.historyWindowLength;
		
		if(tick == 0 || tick == 1){return 0;}
		
		int length = Math.min(historyWindowLength, tick);
		
		int[] historyArray;	
		int[] historyWindowArray = new int[length];
		int[] GradientsArray; 
		int[] smoothedGradientsArray; 
		for(int i=0 ; i<length; i++ )
			historyWindowArray[i] = HistoryDemandArray[tick - length + i];
		
		historyArray = runHoltWinterAlg(historyWindowArray);
		GradientsArray = getGradientsArray(historyArray);
		smoothedGradientsArray = runHoltWinterAlg(GradientsArray);
		
		return smoothedGradientsArray[smoothedGradientsArray.length -1];
	}
	
	protected void updateLoadOnCloud(Map<VirtualMachine, Integer> cpuDemandOfVMs,
			Map<VirtualMachine, Integer> memDemandOfVMs) {
		for(Host host : hosts) { 
			updateHostLoad(host,hostManager.getVmsAtHost(host),cpuDemandOfVMs,memDemandOfVMs);
		}
	}
	
	protected void updateHostLoad(Host hostToUpdate,
			List<VirtualMachine> vmListToUpdate,
			Map<VirtualMachine, Integer> cpuDemandOfVMs, 
			Map<VirtualMachine, Integer> memDemandOfVMs) {
		int sumMinDemand = 0;
		for(VirtualMachine vm : vmListToUpdate) {
			int minDEmand = vm.getVMConfiguration().getMinCpu();
			vm.setSatisfiedCpuDemand(minDEmand);
			sumMinDemand += minDEmand;
		}
		try {
			hostManager.setHostCurrentCpuLoad(hostToUpdate, sumMinDemand);
		} catch (Exception e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
			System.exit(1);
		} 
		int vmNotSatisfied,tmpLoad;
		vmNotSatisfied = 0;
		//count UnSatisfied CPU demand of vm's
		for(VirtualMachine vm : vmListToUpdate) {
			tmpLoad = vm.getSatisfiedCpuDemand();
//			vmsTotalLoad += tmpLoad;
			if(cpuDemandOfVMs.get(vm).intValue()>tmpLoad)
				vmNotSatisfied++;
		}
		//give the rest of cpu demand 
		int loadToShare = hostManager.getCpuAvailableAtHost(hostToUpdate);
		//int loadToShare = hostManager.getCpuCapacityForHost(hostToUpdate)- vmsTotalLoad;
		int cutOfThePie;
		while (vmNotSatisfied > 0 && loadToShare >0) {
			cutOfThePie = loadToShare/vmNotSatisfied;
			if(cutOfThePie!=0) { 
				for(VirtualMachine vm : vmListToUpdate) {
					//int a = cpuDemandOfVMs.get(vm).intValue();
					tmpLoad = cpuDemandOfVMs.get(vm).intValue()- vm.getSatisfiedCpuDemand();
					if(tmpLoad>0) {
						tmpLoad = Math.min(cutOfThePie,tmpLoad);
						vm.updateSatisfiedCpuDemand(tmpLoad);
						try {
							hostManager.updateHostCurrentCpuLoad(hostToUpdate,tmpLoad);
						} catch (Exception e) {
							// TODO Auto-generated catch block
							e.printStackTrace();
						}
						loadToShare -= tmpLoad;
						if((cpuDemandOfVMs.get(vm).intValue()- vm.getSatisfiedCpuDemand())<=0)
							vmNotSatisfied--;
					}
				}
			} else {
				loadToShare =0;
			}
		}
	}
	
	private int[] runHoltWinterAlg(int[] ArrayTosmooth){
		int length = ArrayTosmooth.length;
		double alfa = configuration.alfaInputForHWalgo;
		int[] smoothedArray = new int[length];
		//initial condition.
		smoothedArray[0] = ArrayTosmooth[0];
		
		//algorithm function: s(t) = alfa*x(t-1) + (1-alfa)*s(t-1)
		for(int i=1; i< length ;i++ ){
			smoothedArray[i] = (int) (alfa * ArrayTosmooth[i-1] + (1-alfa) * smoothedArray[i-1]);  
		}
		
		return smoothedArray;
	}
	
	private int[] getGradientsArray(int[] inputArray){
		int k = (int)(inputArray.length * configuration.gradientwidth);
		int outputArray[] = new int[k];
		for(int i=0; i<k ;i++)
			outputArray[i] = (int)((inputArray[i+k] - inputArray[i]) / k);
		
		return outputArray;
	}
	
	protected void cpuReservedCheck() {
		//TODO how to calculate reserved + exception
		int cloudCpuCapacity = hostManager.getTotalCpuCapacity();
		int cpuNeedToReserved =(int) (cloudCpuCapacity* 0.1* configuration.powerPercent);
		int cpuTrueReserved = cloudCpuCapacity-hostManager.getToatalCpuLoadOnCloud();
		int cpuDiff = cpuTrueReserved - cpuNeedToReserved;
		if(cpuDiff>0) {
			hostManager.closeHostsWithCpuSumOf(cpuDiff);
		}	
	}

	protected void validatePlacement() {
		for(Host host :hosts) {
			if(host.getHostCapacity().getCpuCapacity()<host.getCurrentCpuLoad()){
				System.out.println("1: Cpu Capacity");
				System.out.println("Host: "+host.getHostId()+", Cpu Capacity: "+host.getHostCapacity().getCpuCapacity()+
						", Current Cpu Load: "+host.getCurrentCpuLoad());
			}if(host.getHostCapacity().getMemCapacity()<host.getCurrentMemLoad()){
				System.out.println("2: Mem Capacity");
				System.out.println("Host: "+host.getHostId()+", Mem Capacity: "+host.getHostCapacity().getMemCapacity()+
						", Current Mem Load: "+host.getCurrentMemLoad());
			}
			int sumOfCpu = 0;
			int sumOfMem = 0;
			if(host.isOpen()) {
				for(VirtualMachine vm : hostManager.getVmsAtHost(host)) {
					sumOfCpu += vm.getSatisfiedCpuDemand();
					sumOfMem += vm.getSatisfiedMemDemand();
				}
			}
			if(host.getCurrentCpuLoad()!=sumOfCpu){
				System.out.println("3: Cpu Load");
				System.out.println("Host: "+host.getHostId()+", Cpu Load at Host: "+host.getCurrentCpuLoad()+
						", sum Of Cpu from vm: "+sumOfCpu);
			}if(host.getCurrentMemLoad()!=sumOfMem){
				System.out.println("4: Mem Load");
				System.out.println("Host: "+host.getHostId()+", Mem Load at Host: "+host.getCurrentMemLoad()+
						", sum Of Mem from vm: "+sumOfMem);}
		}
		
	} 
	
	
	protected double calcAvailableCpu() {
		double totalAvCpu = 0.0;
		double redLine = configuration.HostRedLine;
		double minSmallVm = configuration.smallVmCpuMin;
		double hostRedLine,avCpu;
		for(Host host : hostManager.getOpenHosts()) {
			hostRedLine = (redLine/100.0)*(host.getHostCapacity().getCpuCapacity());
			avCpu = hostRedLine - host.getCurrentCpuLoad();
			if(avCpu>minSmallVm)
				totalAvCpu += avCpu;
				
		}
		return totalAvCpu;
	}
	
	//y2 = (grad *(x2-x1))+y1
	//y2 is estimate demand, x2-x1 is the distance between ticks
	protected double getEstimateDemandToNextTIck(int currentTick) {
		double y1 = HistoryDemandArray[currentTick];
		double grad = calcCloudGraphGradient();
		return grad+y1;
	}
	
	protected double getTotalCpuCapacityOnOpenHost() {
		double totalCapc = 0.0;
		for(Host host : hostManager.getOpenHosts()) 
			totalCapc += host.getHostCapacity().getCpuCapacity();
		return totalCapc;
	}
	
	protected boolean openNewHost() {
		double totalCapc = getTotalCpuCapacityOnOpenHost();
		double estimateDemand = getEstimateDemandToNextTIck(tick);
		//if we estimate that we have enough capacity no need to open
		if(totalCapc <= estimateDemand)
			return estimateCost(totalCapc,estimateDemand);
		return false;
	}

	private boolean estimateCost(double totalCapc, double estimateDemand) {
		double delta = estimateDemand - totalCapc;
		if(delta<0)
			return false;
		return hostManager.openNewHostWithMinCpu(delta, tick);
	}
	
	protected void closeHost() {
		int grad = calcCloudGraphGradient();
		if(grad>=0)
			return;
		for(Host host :hostManager.getOpenHosts()) {
			if(host.getCurrentCpuLoad()==0)
				hostManager.closeHost(host);
		}
	}
	
	protected Host getFirstFitHost(VirtualMachine vmToBeDeployed,
			Map<VirtualMachine, Integer> cpuDemandOfVMs2,
			Map<VirtualMachine, Integer> memDemandOfVMs,int tick) {
		this.sortHostList.sortHostList(hosts);
		int bestScore = Integer.MIN_VALUE;
		Host bestHost = null;
		for(Host newHost : hosts) {
			int tmpScore = hostEvaluator.evaluationForHost(newHost, vmToBeDeployed,
					hostManager.getVmsAtHost(newHost),cpuDemandOfVMs2,memDemandOfVMs);
			if(tmpScore>0 && newHost.isOpen())
				return newHost;
			if(tmpScore != Integer.MIN_VALUE && tmpScore>bestScore && newHost.isOpen()) {
				bestScore = tmpScore;
				bestHost = newHost;
			}
		}
		//TODO check if have X  free space at cloud
		//if no - open new host 
		//if yes return bestHost
		if(bestHost!= null && bestScore!= Integer.MIN_VALUE)
			return bestHost; 	
//		Host newHost = hostManager.openNewHost(tick);
		//if(newHost!=null)
		//hosts.add(newHost);
		return null;
	}
	

	protected int getTotalCpuDemand() {
		int totalDemand = 0;
		for(Host host :vmMapping.keySet()) {
			for(VirtualMachine vm :vmMapping.get(host)) {
				totalDemand += demandManager.getCPUDemandOfVmAtTick(vm, tick);
			}
		}
		return totalDemand;
	}
}
